package dk.javacode.srsm;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import dk.javacode.srsm.exceptions.MappingException;
import dk.javacode.srsm.util.ReflectionUtil;

public class ResultSetMapper {

	private static final Map<Class<?>, TableDescriptor> descriptors = new HashMap<Class<?>, TableDescriptor>();

	private static final ReentrantLock buildDescriptorLock = new ReentrantLock();

	private final Map<Class<?>, Object> refs = new HashMap<Class<?>, Object>();
	
	private String tableSplitToken = "__";
	
	public ResultSetMapper() {
		super();
	}

	public ResultSetMapper(String tableSplitToken) {
		super();
		this.tableSplitToken = tableSplitToken;
	}

	//@formatter:off
	/**
	 * <pre>
	 * Maps a resultSet to a POJO.
	 * The class to map to must be annotated with SRTable and SRColumn annotations
	 * to indicate table and column names. (OK, so tableName is actually not being
	 * used for this mapping, but it is still required)
	 * 
	 * All columns from the result set will be attempted to be mapped.
	 * Fields of referenced (indirect) objects must be labelled "[referencedType]__[column]".
	 * 
	 * I.e. a Person has an Address field (private Address address). To instantiate the address
	 * the resultSet columns for address values must be prefixed address__ (example address__street_name).
	 * And the address field must have the columnReference annotation set to the id of the Address class.
	 * 
	 * If only the id of a referenced object is retrieved it is still instantiated, but only the ID field
	 * will be initialised.
	 * 
	 * In general, objects will be created even if not all columns are available. Only as much as is possible
	 * from the data in the resultSet is initialised.
	 * </pre>
	 * 
	 * @param clazz The type to map this result set to
	 * @param resultSet The result set to map
	 * @return A list of instances of the type to map to. One instance per row
	 * @throws SQLException
	 * @throws MappingException
	 */
	//@formatter:on
	public <E> List<E> toPojo(Class<E> clazz, ResultSet resultSet) throws SQLException, MappingException {
		List<E> result = new ArrayList<E>();

		// Build the tableDescriptor for this Class/Type
		TableDescriptor	tableDescriptor = TableDescriptor.build(clazz);

		// Read metaData from the result set
		ResultSetMetaData metaData = resultSet.getMetaData();
		int columnCount = metaData.getColumnCount();

		// Create a pojo for each row in the result set
		while (resultSet.next()) {
			E pojo = ReflectionUtil.getNewInstance(clazz);
			for (int column = 1; column <= columnCount; column++) {
				// int type = metaData.getColumnType(column);
				Object columnValue = resultSet.getObject(column);
//				if (columnValue != null) {
					String columnname = metaData.getColumnLabel(column).toLowerCase();
					updatePojo(pojo, clazz, columnname, tableDescriptor, columnValue);
//				}
			}
			result.add(pojo);
			refs.clear();
		}
		return result;
	}

	/**
	 * <pre>
	 * Updates a field of a pojo or calls recursively to update the field of a referenced pojo.
	 * </pre>
	 * 
	 * @param pojo
	 * @param clazz
	 * @param columnName
	 * @param resultSet
	 * @param tableDescriptor
	 * @param column
	 * @throws SQLException
	 * @throws MappingException
	 */
	private <E> void updatePojo(Object pojo, Class<E> clazz, String columnName, TableDescriptor tableDescriptor,
			Object columnValue) throws SQLException, MappingException {
		ColumnDescriptor columnDescriptor = tableDescriptor.getColumnDescriptor(columnName);
		if (columnDescriptor == null) { 
			// look for column descriptor in referenced types
			int in = columnName.indexOf(tableSplitToken);
			if (in < 1) {
				throw new MappingException("No column descriptor found for column: " + columnName);
			}
			String refTable = columnName.substring(0, in);
			String refColumn = columnName.substring(in + 2);

			TableDescriptor referencedTable = tableDescriptor.getReferencedTypes().get(refTable);
			Class<?> refType = referencedTable.getMappedClass();
			columnDescriptor = referencedTable.getColumnDescriptor(refColumn);
			Object ref = refs.get(refType);
			if (ref == null) {
				ref = ReflectionUtil.getNewInstance(refType);
			}
			updatePojo(ref, refType, refColumn, referencedTable, columnValue);
		} else { 
			// Column descriptor found
			Field field = columnDescriptor.getColumnField();
			columnValue = columnDescriptor.getDataConverter().convertToPojo(columnValue);

			if (columnDescriptor.getColumnReference() != null && columnValue != null) {
				Class<?> referencedType = field.getType();
				Object ref = refs.get(referencedType);
				if (ref == null) {
					ref = ReflectionUtil.getNewInstance(referencedType);
				}
				Method refMethod = columnDescriptor.getColumnReferenceSetter();
				ReflectionUtil.invokeMethod(ref, refMethod, columnValue);
				ReflectionUtil.invokeMethod(pojo, columnDescriptor.getColumnSetMethod(), ref);
				refs.put(ref.getClass(), ref);
			} else {
				ReflectionUtil.invokeMethod(pojo, columnDescriptor.getColumnSetMethod(), columnValue);
			}
		}
	}

	public static void clearDescriptors() {
		descriptors.clear();
	}

}
